Wire auth and locale to shared.json, AI settings to cli.json#2821
Wire auth and locale to shared.json, AI settings to cli.json#2821bcotrim wants to merge 12 commits intostu-1350-decoupled-config-devfrom
Conversation
Move auth token reads/writes from appdata to shared-config for Desktop+CLI. Move locale reads/writes to shared-config (Desktop saves, CLI reads). Move aiProvider and anthropicApiKey from shared-config to cli-config (CLI-only). Remove auth and locale fields from appdata schema and types since they're now in separate files. Update all imports and tests accordingly. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
fredrikekelund
left a comment
There was a problem hiding this comment.
Left some comments. Shared config error handling and using _events instead of a shared config file watcher are the big things we need to address, IMO.
Haven't tested yet but will do that now 👍
| if ( error instanceof z.ZodError || error instanceof SyntaxError ) { | ||
| return structuredClone( DEFAULT_SHARED_CONFIG ); | ||
| } | ||
| throw new Error( 'Failed to read shared config file.' ); |
There was a problem hiding this comment.
Same as in apps/cli/lib/cli-config/core.ts, we should look for differing version props here and say something like "A newer version of Studio or the Studio CLI is installed on your system" if the version differs.
| } catch { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
This relates to the error handling I suggested in readSharedConfig. I'm not sure what the best approach is here…
If the user is running an old version of the CLI but have a newer version of Studio installed (that uses a new schema for the shared config file), then we should prompt them to upgrade.
However, we still need both programs to recover from errors well enough to at least report the right thing to the user. This is especially true for Studio, which needs to be able to auto-update as usual.
tools/common/lib/shared-config.ts
Outdated
|
|
||
| const sharedConfigSchema = z | ||
| .object( { | ||
| version: z.number().default( 1 ), |
There was a problem hiding this comment.
Let's put the version in a constant and add a comment explaining that any schema update must maintain backwards compatibility. If the change is breaking, the version should be updated and a data migration function should be added.
There was a problem hiding this comment.
The important thing to test here, IMO, is how we recover from schema-related errors. We have some of that in the readSharedConfig tests, but I think we can expand on that (along with updating the logic in the actual source code)
apps/cli/lib/cli-config/core.ts
Outdated
| await unlockFileAsync( LOCKFILE_PATH ); | ||
| } | ||
|
|
||
| export async function updateCliConfig( |
There was a problem hiding this comment.
| export async function updateCliConfig( | |
| export async function updateCliConfigWithPartial( |
Just a suggestion
tools/common/lib/shared-config.ts
Outdated
| } | ||
| } | ||
|
|
||
| export async function readAuthToken(): Promise< StoredToken | null > { |
There was a problem hiding this comment.
Exporting functions that return just parts of the config is clearly great for reducing the amount of test mocks. Nice 👍
tools/common/lib/shared-config.ts
Outdated
| displayName: z.string().default( '' ), | ||
| } ); | ||
|
|
||
| export type StoredToken = z.infer< typeof authTokenSchema >; |
There was a problem hiding this comment.
| export type StoredToken = z.infer< typeof authTokenSchema >; | |
| export type StoredAuthToken = z.infer< typeof authTokenSchema >; |
I'd include "auth" in the name somehow
There was a problem hiding this comment.
Ouff… I didn't consider the HTTPS certificates. We should put these in ~/.studio, too, IMO. Let's do that in a separate PR, though.
apps/cli/lib/certificate-manager.ts
Outdated
| function getAppdataDirectory(): string { | ||
| if ( process.env.E2E && process.env.E2E_APP_DATA_PATH ) { | ||
| return path.join( process.env.E2E_APP_DATA_PATH, 'Studio' ); | ||
| } | ||
|
|
||
| if ( process.platform === 'win32' ) { | ||
| if ( ! process.env.APPDATA ) { | ||
| throw new LoggerError( __( 'Studio config file path not found.' ) ); | ||
| } | ||
|
|
||
| return path.join( process.env.APPDATA, 'Studio' ); | ||
| } | ||
|
|
||
| return path.join( os.homedir(), 'Library', 'Application Support', 'Studio' ); | ||
| } |
There was a problem hiding this comment.
Could we use getAppdataDirectory from apps/cli/lib/server-files.ts here?
There was a problem hiding this comment.
Could we use _events for listening to auth changes, too, instead of watching the shared config file?
fredrikekelund
left a comment
There was a problem hiding this comment.
This works as expected in testing 👍 Error handling for the shared config is really key to get right. I'll still approve now to keep up momentum, since we aren't landing straight to trunk
| if ( ! token ) { | ||
| setIsAuthenticated( false ); | ||
| setClient( undefined ); | ||
| setWpcomClient( undefined ); | ||
| setUser( undefined ); | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
The auth logout command didn't trigger a change in authentication status in the app for me when I tested it the first time. Probably because the shared config file didn't exist when I started Studio. I guess this would be solved by moving to an _events architecture for auth events instead.
There was a problem hiding this comment.
Let's update this file to import aiProviderSchema from apps/cli/lib/cli-config/core.ts. We should have a single definition for that
| isReady: async () => { | ||
| const { anthropicApiKey } = await readCliConfig(); | ||
| return Boolean( anthropicApiKey ); | ||
| }, |
There was a problem hiding this comment.
We need to remember to ping the Architecture team before landing this to trunk, to let them know that they need to manually migrate AI-related props from the appdata file to the CLI config file.
| vi.mock( '@studio/common/lib/shared-config', async ( importOriginal ) => ( { | ||
| ...( await importOriginal< typeof import('@studio/common/lib/shared-config') >() ), | ||
| readAuthToken: vi.fn(), | ||
| } ) ); |
There was a problem hiding this comment.
| vi.mock( '@studio/common/lib/shared-config', async ( importOriginal ) => ( { | |
| ...( await importOriginal< typeof import('@studio/common/lib/shared-config') >() ), | |
| readAuthToken: vi.fn(), | |
| } ) ); | |
| vi.mock( import( '@studio/common/lib/shared-config' ), async ( importOriginal ) => ( { | |
| ...( await importOriginal() ), | |
| readAuthToken: vi.fn(), | |
| } ) ); |
Tip: I stumbled upon this recently, that the first argument to vi.mock can be an import() call, which makes Vitest aware of the module types.
| export type AiProviderId = keyof typeof AI_PROVIDERS; | ||
|
|
||
| export const aiProviderSchema = z.enum( [ 'wpcom', 'anthropic-claude', 'anthropic-api-key' ] ); | ||
| export { aiProviderSchema }; |
There was a problem hiding this comment.
Do we really need to re-export this definition?
|
|
||
| const sharedConfigSchema = z | ||
| .object( { | ||
| version: z.number().default( SHARED_CONFIG_VERSION ), |
There was a problem hiding this comment.
| version: z.number().default( SHARED_CONFIG_VERSION ), | |
| version: z.literal( SHARED_CONFIG_VERSION ), |
I should have called this out in apps/cli/lib/cli-config/core.ts, too, but this is an important distinction: I think the zod schema parsing should fail unless the version field matches what we expect.
If we don't do this, then the schema parsing might fail for other reasons, but it's not as easy for us to see why it's happening.
We can't just change this line to z.literal instead, we also need a base schema containing just the version field to try with. I'm basically imagining the logic we have in apps/cli/lib/cli-config/core.ts, just with z.literal instead of z.number().default()
Related issues
How AI was used in this PR
All code was generated via Claude Code (Opus + Haiku). Changes were reviewed and validated by the author through iterative conversation — verifying architecture decisions, confirming read/write boundaries, and running the full test suite at each step.
Proposed Changes
tools/common/lib/shared-config.ts— new shared config infrastructure at~/.studio/shared.jsonfor data shared between Desktop and CLI (auth token + locale)aiProvider,anthropicApiKey) from appdata tocli.jsonlastBumpStats) from appdata tocli.jsonauthToken,locale, AI fields, andlastBumpStatsfrom appdata types and schemasAfter these changes the write boundaries are:
appdata-v1.json(sites, snapshots, preferences) +shared.json(auth, locale)cli.json(sites, AI settings, telemetry) +shared.json(auth) +appdata-v1.json(snapshots only)Testing Instructions
npm run typecheck— should pass with no errorsnpm test— all 1292 tests should pass~/.studio/shared.jsoncontains the auth token~/.studio/shared.jsoncontains the localestudio auth loginfrom CLI — verify shared.json is updatedstudio aiand switch provider/enter API key — verify~/.studio/cli.jsoncontainsaiProviderandanthropicApiKeyPre-merge Checklist